Leaflet Blog in Deno Fresh
1/** @jsxImportSource preact */
2import { CSS, render } from "@deno/gfm";
3import { Handlers, PageProps } from "$fresh/server.ts";
4
5import { Footer } from "../../components/footer.tsx";
6import { PostInfo } from "../../components/post-info.tsx";
7import { Title } from "../../components/typography.tsx";
8import { getPost } from "../../lib/api.ts";
9import { Head } from "$fresh/runtime.ts";
10
11interface Post {
12 uri: string;
13 value: {
14 title: string;
15 content: string;
16 createdAt: string;
17 };
18}
19
20// Only override backgrounds in dark mode to make them transparent
21const transparentDarkModeCSS = `
22@media (prefers-color-scheme: dark) {
23 .markdown-body {
24 color: white;
25 background-color: transparent;
26 }
27
28 .markdown-body a {
29 color: #58a6ff;
30 }
31
32 .markdown-body blockquote {
33 border-left-color: #30363d;
34 background-color: transparent;
35 }
36
37 .markdown-body pre,
38 .markdown-body code {
39 background-color: transparent;
40 color: #c9d1d9;
41 }
42
43 .markdown-body table td,
44 .markdown-body table th {
45 border-color: #30363d;
46 background-color: transparent;
47 }
48}
49
50.font-sans { font-family: var(--font-sans); }
51.font-serif { font-family: var(--font-serif); }
52.font-mono { font-family: var(--font-mono); }
53
54.markdown-body h1 {
55 font-family: var(--font-serif);
56 text-transform: uppercase;
57 font-size: 2.25rem;
58}
59
60.markdown-body h2 {
61 font-family: var(--font-serif);
62 text-transform: uppercase;
63 font-size: 1.75rem;
64}
65
66.markdown-body h3 {
67 font-family: var(--font-serif);
68 text-transform: uppercase;
69 font-size: 1.5rem;
70}
71
72.markdown-body h4 {
73 font-family: var(--font-serif);
74 text-transform: uppercase;
75 font-size: 1.25rem;
76}
77
78.markdown-body h5 {
79 font-family: var(--font-serif);
80 text-transform: uppercase;
81 font-size: 1rem;
82}
83
84.markdown-body h6 {
85 font-family: var(--font-serif);
86 text-transform: uppercase;
87 font-size: 0.875rem;
88}
89`;
90
91export const handler: Handlers<Post> = {
92 async GET(_req, ctx) {
93 try {
94 const { slug } = ctx.params;
95 const post = await getPost(slug);
96 return ctx.render(post);
97 } catch (error) {
98 console.error("Error fetching post:", error);
99 return new Response("Post not found", { status: 404 });
100 }
101 },
102};
103
104export default function BlogPage({ data: post }: PageProps<Post>) {
105 if (!post) {
106 return <div>Post not found</div>;
107 }
108
109 return (
110 <>
111 <Head>
112 <title>{post.value.title} — knotbin</title>
113 <meta name="description" content="by Roscoe Rubin-Rottenberg" />
114 {/* Merge GFM’s default styles with our dark-mode overrides */}
115 <style
116 dangerouslySetInnerHTML={{ __html: CSS + transparentDarkModeCSS }}
117 />
118 </Head>
119
120 <div className="grid grid-rows-[20px_1fr_20px] justify-items-center min-h-dvh py-8 px-4 xs:px-8 pb-20 gap-16 sm:p-20">
121 <link rel="alternate" href={post.uri} />
122 <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start w-full max-w-[600px] overflow-hidden">
123 <article className="w-full space-y-8">
124 <div className="space-y-4 w-full">
125 <a
126 href="/"
127 className="hover:underline hover:underline-offset-4 font-medium"
128 >
129 Back
130 </a>
131 <Title>{post.value.title}</Title>
132 <PostInfo
133 content={post.value.content}
134 createdAt={post.value.createdAt}
135 includeAuthor
136 className="text-sm"
137 />
138 <div className="diagonal-pattern w-full h-3" />
139 </div>
140 <div className="[&>.bluesky-embed]:mt-8 [&>.bluesky-embed]:mb-0">
141 {/* Render GFM HTML via dangerouslySetInnerHTML */}
142 <div
143 class="mt-8 markdown-body"
144 dangerouslySetInnerHTML={{ __html: render(post.value.content) }}
145 />
146 </div>
147 </article>
148 </main>
149 <Footer />
150 </div>
151 </>
152 );
153}